using System;
using System.Diagnostics;

namespace RSEngine.Math
{
    /// <summary>
    ///	A 3D box aligned with the x/y/z axes.
    /// </summary>
    /// <remarks>
    ///	This class represents a simple box which is aligned with the
    ///	axes. Internally it only stores 2 points as the extremeties of
    ///	the box, one which is the minima of all 3 axes, and the other
    ///	which is the maxima of all 3 axes. This class is typically used
    ///	for an axis-aligned bounding box (AABB) for collision and
    ///	visibility determination.
    /// </remarks>
    public struct AxisAlignedBox
    {

        #region Variables

        internal Vector3 _minVector;
        internal Vector3 _maxVector;

        private Vector3[] _corners;

        public static readonly AxisAlignedBox Invalid = new AxisAlignedBox(Vector3.Invalid, Vector3.Invalid);
        public static readonly AxisAlignedBox Unit = new AxisAlignedBox(new Vector3(-1, -1, -1), new Vector3(1, 1, 1));

        #endregion

        #region Constructors

        public AxisAlignedBox(Vector3 min, Vector3 max)
        {
            _corners = new Vector3[8];
            _minVector = Vector3.Invalid;
            _maxVector = Vector3.Invalid;

            SetExtents(min, max);
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets/Sets the size of the box.
        /// </summary>
        public Vector3 Size
        {
            get
            {
                return _maxVector - _minVector;
            }
            set
            {
                Vector3 center = Center;
                Vector3 halfSize = .5f * value;
                _minVector = center - halfSize;
                _maxVector = center + halfSize;
            }
        }

        /// <summary>
        /// Gets the center point of this bounding box.
        /// </summary>
        public Vector3 Center
        {
            get
            {
                return (_minVector + _maxVector) * 0.5f;
            }
            set
            {
                Vector3 halfSize = .5f * Size;
                _minVector = value - halfSize;
                _maxVector = value + halfSize;
            }
        }

        /// <summary>
        ///	Gets/Sets the maximum corner of the box.
        /// </summary>
        public Vector3 Maximum
        {
            get
            {
                return _maxVector;
            }
            set
            {
                _maxVector = value;
                UpdateCorners();
            }
        }

        /// <summary>
        ///	Gets/Sets the minimum corner of the box.
        /// </summary>
        public Vector3 Minimum
        {
            get
            {
                return _minVector;
            }
            set
            {
                _minVector = value;
                UpdateCorners();
            }
        }

        /// <summary>
        ///	Returns an array of 8 corner points, useful for
        ///	collision vs. non-aligned objects.
        ///	</summary>
        ///	<remarks>
        ///	If the order of these corners is important, they are as
        ///	follows: The 4 points of the minimum Z face (note that
        ///	because we use right-handed coordinates, the minimum Z is
        ///	at the 'back' of the box) starting with the minimum point of
        ///	all, then anticlockwise around this face (if you are looking
        ///	onto the face from outside the box). Then the 4 points of the
        ///	maximum Z face, starting with maximum point of all, then
        ///	anticlockwise around this face (looking onto the face from
        ///	outside the box). Like this:
        ///	<pre>
        ///			 1-----2
        ///		    /|     /|
        ///		  /  |   /  |
        ///		5-----4   |
        ///		|   0-|--3
        ///		|  /   |  /
        ///		|/     |/
        ///		6-----7
        ///	</pre>
        /// </remarks>
        public Vector3[] Corners
        {
            get
            {
                return (Vector3[])_corners.Clone();
            }
        }

        /// <summary>
        /// Gets the surface area of the box.
        /// </summary>
        public float Area
        {
            get
            {
                Vector3 w = Size;

                float ax = System.Math.Max(w.x, 0);
                float ay = System.Math.Max(w.y, 0);
                float az = System.Math.Max(w.z, 0);

                return 2 * (ax * ay + ay * az + az * ax);
            }
        }

        /// <summary>
        /// Gets the box's volume.
        /// </summary>
        public float Volume
        {
            get
            {
                Vector3 w = Size;

                float ax = System.Math.Max(w.x, 0);
                float ay = System.Math.Max(w.y, 0);
                float az = System.Math.Max(w.z, 0);

                return ax * ay * az;
            }
        }

        /// <summary>
        ///	Gets/Sets the value of whether this box is invalid.
        /// </summary>
        public bool IsInvalid
        {
            get
            {
                return _minVector.IsInvalid || _maxVector.IsInvalid;
            }
            set
            {
                if (value)
                {
                    _minVector = Vector3.Invalid;
                    _maxVector = Vector3.Invalid;
                }
            }
        }

        #endregion

        #region Transform

        public void Transform(Matrix4 matrix)
        {
            // do nothing for a null box
            if (IsInvalid)
                return;

            Vector3 min = new Vector3();
            Vector3 max = new Vector3();

            // Transform and check extents
            for (int i = 0; i < _corners.Length; i++)
            {
                Vector3 current = matrix * _corners[i];

                if (i == 0 || current.x > max.x)
                    max.x = current.x;
                if (i == 0 || current.y > max.y)
                    max.y = current.y;
                if (i == 0 || current.z > max.z)
                    max.z = current.z;
                if (i == 0 || current.x < min.x)
                    min.x = current.x;
                if (i == 0 || current.y < min.y)
                    min.y = current.y;
                if (i == 0 || current.z < min.z)
                    min.z = current.z;
            }

            SetExtents(min, max);
        }

        #endregion

        #region FromDimensions

        public static AxisAlignedBox FromDimensions(Vector3 center, Vector3 size)
        {
            Vector3 halfSize = .5f * size;
            return new AxisAlignedBox(center - halfSize, center + halfSize);
        }

        #endregion

        #region Merge

        /// <summary>
        ///	Allows for merging two boxes together (combining).
        /// </summary>
        /// <param name="box">The source box.</param>
        public void Merge(AxisAlignedBox box)
        {
            // nothing to merge with in this case, just return
            if (box.IsInvalid)
                return;
            else if (IsInvalid)
                SetExtents(box.Minimum, box.Maximum);
            else
            {
                Vector3 min = _minVector;
                Vector3 max = _maxVector;
                box.Minimum = min.Floor(box.Minimum);
                box.Maximum = max.Ceil(box.Maximum);
            }
        }

        /// <summary>
        ///	Extends the box to encompass the specified point (if needed).
        /// </summary>
        /// <param name="point">The point</param>
        public void Merge(Vector3 point)
        {
            if (IsInvalid)
            {
                // if null, use this point
                SetExtents(point, point);
            }
            else
            {
                Maximum = Maximum.Ceil(point);
                Minimum = Minimum.Floor(point);
            }
        }

        #endregion

        #region UpdateCorners

        private void UpdateCorners()
        {
            // The order of these items is, using right-handed co-ordinates:
            // Minimum Z face, starting with Min(all), then anticlockwise
            //   around face (looking onto the face)
            // Maximum Z face, starting with Max(all), then anticlockwise
            //   around face (looking onto the face)
            _corners[0] = _minVector;
            _corners[1].x = _minVector.x;
            _corners[1].y = _maxVector.y;
            _corners[1].z = _minVector.z;
            _corners[2].x = _maxVector.x;
            _corners[2].y = _maxVector.y;
            _corners[2].z = _minVector.z;
            _corners[3].x = _maxVector.x;
            _corners[3].y = _minVector.y;
            _corners[3].z = _minVector.z;

            _corners[4] = _maxVector;
            _corners[5].x = _minVector.x;
            _corners[5].y = _maxVector.y;
            _corners[5].z = _maxVector.z;
            _corners[6].x = _minVector.x;
            _corners[6].y = _minVector.y;
            _corners[6].z = _maxVector.z;
            _corners[7].x = _maxVector.x;
            _corners[7].y = _minVector.y;
            _corners[7].z = _maxVector.z;
        }

        #endregion

        #region SetExtents

        /// <summary>
        ///	Sets both Minimum and Maximum at once, so that UpdateCorners only
        ///	needs to be called once as well.
        /// </summary>
        /// <param name="min"></param>
        /// <param name="max"></param>
        public void SetExtents(Vector3 min, Vector3 max)
        {
            _minVector = min;
            _maxVector = max;

            UpdateCorners();
        }

        #endregion

        #region Scale

        /// <summary>
        /// Scales the size of the box by the supplied factor.
        /// </summary>
        /// <param name="factor">Factor of scaling to apply to the box.</param>
        public void Scale(Vector3 factor)
        {
            Vector3 min = _minVector * factor;
            Vector3 max = _maxVector * factor;

            SetExtents(min, max);
        }

        #endregion

        #region Intersection calculation

        /// <summary>
        ///	Returns whether or not this box intersects another.
        /// </summary>
        /// <param name="box2">The second box</param>
        /// <returns>True if the 2 boxes intersect, false otherwise.</returns>
        public bool Intersects(AxisAlignedBox box2)
        {
            // Early-fail for nulls
            if (this.IsInvalid || box2.IsInvalid)
                return false;

            // Use up to 6 separating planes
            if (this._maxVector.x < box2._minVector.x)
                return false;
            if (this._maxVector.y < box2._minVector.y)
                return false;
            if (this._maxVector.z < box2._minVector.z)
                return false;

            if (this._minVector.x > box2._maxVector.x)
                return false;
            if (this._minVector.y > box2._maxVector.y)
                return false;
            if (this._minVector.z > box2._maxVector.z)
                return false;

            // otherwise, must be intersecting
            return true;
        }

        /// <summary>
        ///	Tests whether the vector point is within this box.
        /// </summary>
        /// <param name="vector">The point</param>
        /// <returns>True if the vector is within this box, false otherwise.</returns>
        public bool IsInside(Vector3 vector)
        {
            return (vector.x >= _minVector.x && vector.x <= _maxVector.x &&
                vector.y >= _minVector.y && vector.y <= _maxVector.y &&
                vector.z >= _minVector.z && vector.z <= _maxVector.z);
        }

        #endregion

        #region Operators

        public static bool operator ==(AxisAlignedBox left, AxisAlignedBox right)
        {
            if ((object.ReferenceEquals(left, null) || left.IsInvalid) &&
                (object.ReferenceEquals(right, null) || right.IsInvalid))
                return true;
            else if ((object.ReferenceEquals(left, null) || left.IsInvalid) ||
                     (object.ReferenceEquals(right, null) || right.IsInvalid))
                return false;

            return
                (left._corners[0] == right._corners[0] && left._corners[1] == right._corners[1] && left._corners[2] == right._corners[2] &&
                left._corners[3] == right._corners[3] && left._corners[4] == right._corners[4] && left._corners[5] == right._corners[5] &&
                left._corners[6] == right._corners[6] && left._corners[7] == right._corners[7]);
        }

        public static bool operator !=(AxisAlignedBox left, AxisAlignedBox right)
        {
            if ((object.ReferenceEquals(left, null) || left.IsInvalid) &&
                (object.ReferenceEquals(right, null) || right.IsInvalid))
                return false;
            else if ((object.ReferenceEquals(left, null) || left.IsInvalid) ||
                     (object.ReferenceEquals(right, null) || right.IsInvalid))
                return true;

            return
                (left._corners[0] != right._corners[0] || left._corners[1] != right._corners[1] || left._corners[2] != right._corners[2] ||
                left._corners[3] != right._corners[3] || left._corners[4] != right._corners[4] || left._corners[5] != right._corners[5] ||
                left._corners[6] != right._corners[6] || left._corners[7] != right._corners[7]);
        }

        public override bool Equals(object obj)
        {
            return obj is AxisAlignedBox && this == (AxisAlignedBox)obj;
        }

        #endregion

        #region Misc

        public override int GetHashCode()
        {
            if (IsInvalid)
                return 0;

            return _corners[0].GetHashCode() ^ _corners[1].GetHashCode() ^ _corners[2].GetHashCode() ^ _corners[3].GetHashCode() ^ _corners[4].GetHashCode() ^
                _corners[5].GetHashCode() ^ _corners[6].GetHashCode() ^ _corners[7].GetHashCode();
        }

        public override string ToString()
        {
            return this._minVector.ToString() + ":" + this._maxVector.ToString();
        }

        public AxisAlignedBox Clone()
        {
            return new AxisAlignedBox(_minVector, _maxVector);
        }

        #endregion

    }
}
